#pragma once

#include <cstdint>

#include <string>
#include <vector>
#include <iostream>

#include "point.hpp"
#include "noise.hpp"

#include <json.hpp>

// Road Width in map
#define ROUTHWIDTH 2

namespace dw
{
    class Map
    {
    private:
        uint32_t id;
        uint32_t row;
        uint32_t col;
        uint8_t *buf;

    public:
        struct State
        {
            uint32_t id = 0;
            uint32_t row = 0;
            uint32_t col = 0;
            std::string map;

        public:
            bool isValidCoord(const Coord &p) const
            {
                return p.x >= 0 && p.x < double(col) &&
                       p.y >= 0 && p.y < double(row);
            }

            bool isValidGrid(const Grid &p) const
            {
                return p.x >= 0 && p.x < int(col) &&
                       p.y >= 0 && p.y < int(row);
            }

            char &operator[](const Grid &p)
            {
                if (!isValidGrid(p))
                    return map[row * col];
                return map[p.y * col + p.x];
            }

            const char operator[](const Grid &p) const
            {
                if (!isValidGrid(p))
                    return map[row * col];
                return map[p.y * col + p.x];
            }

            bool contains(const Coord& p) const {
                return p.x >= 0 && p.x < col &&
                       p.y >= 0 && p.y < row;
            }

            bool isBlock(const Grid &p) const
            {
                return !contains(p) || '0' != map[p.y * col + p.x];
            }

        public:
            // Rescale the map
            // scale: new block is 1 / scale^2 origin block
            Map::State scale(const int scalar);

            // Expand the obstacle
            // radius: block num spreads from the obstacle
            Map::State expand(const int radius);
        
        public:
            Coord getAccessableCoord() const;

        public:
            /* json */
            friend void to_json(nlohmann::json &j, const Map::State &state);
            friend void from_json(const nlohmann::json &j, Map::State &state);

            // Encode Map::State to json-style string
            std::string encode() const;

            // decode Map::State from json-style string
            Map::State &decode(const std::string& data_msg);

            /* Run-Length Encoding */
            // Encode Map::State::map to Run-Length Encoding
            std::string compress() const;

            // Decode Map::State::map from Run-Length Encoding
            Map::State &decompress(const std::string &str);

            /* Character Drawing */
            friend std::ostream &operator<<(std::ostream &os, const Map::State &map_state);
        };

    public:
        static const std::string block[];

        enum Color
        {
            WHITE,
            BLACK,
            RED,
            GREEN,
            BROWN,
            BLUE,
            PINK,
            CYAN,
            DEFAULT
        };

        enum class Type
        {
            PERLIN,
            MAZE
        };

        enum class Direction 
        {
            UP,
            DOWN,
            LEFT,
            RIGHT
        };

    public:
        Map()
        {
            this->id = 0;
            this->row = 0;
            this->col = 0;
            this->buf = new uint8_t{0};
        }

        Map(const Map &_map)
        {
            this->id = _map.id;
            this->row = _map.row;
            this->col = _map.col;
            this->buf = new uint8_t[row * col + 1];
            memcpy((void *)this->buf, (const void *)_map.buf, row * col * sizeof(uint8_t));
            buf[row * col] = 0;
        }

        // Construct Map from Map::State
        Map(const Map::State &_state)
        {
            this->id = _state.id;
            this->row = _state.row;
            this->col = _state.col;

            this->buf = new uint8_t[_state.row * _state.col + 1];

            for (uint32_t i = 0; i < this->row; i++)
            {
                for (uint32_t j = 0; j < this->col; j++)
                {
                    this->buf[i * this->col + j] = _state.map[i * this->col + j] - '0';
                }
            }

            buf[row * col] = 0;
        }

        ~Map()
        {
            delete[] this->buf;
        }

    public:
        Map &set(const Map::State &_state);
    
    public:
        // Input: row, col
        // Default: Perlin Noise Generate
        Map(uint32_t _row, uint32_t _col, Map::Type _type = Map::Type::PERLIN, uint32_t _id = 0);

        bool generate(const Map::Type _type, float WhitePercent = 0.0f);

    private:
        // Generate map by Perlin Noise
        // Input: Percentage of Road Space in total map Space
        bool PerlinGenerate(float WhitePercent = 0.0f);

        // Generate Random Maze
        bool MazeGenerate();

    public:
        Coord getSpawnPoint();

    public:
        // return {col, row} {x, y}
        olc::vu2d size() const { return {col, row}; }
        uint32_t Row() const { return row; }
        uint32_t Col() const { return col; }

        bool empty() const { return !(row * col); };
        operator bool() { return !empty(); };

        bool isValidCoord(const Coord &p) const
        {
            return p.x >= 0 && p.x <= col &&
                   p.y >= 0 && p.y <= row;
        }

        bool isValidGrid(const Grid &p) const
        {
            return p.x >= 0 && p.x < int(col) &&
                   p.y >= 0 && p.y < int(row);
        }

        uint8_t &operator[](const Grid &p)
        {
            if (!isValidGrid(p))
                return buf[row * col];
            return buf[p.y * col + p.x];
        }

        const uint8_t operator[](const Grid &p) const
        {
            if (!isValidGrid(p))
                return 0;
            return buf[p.y * col + p.x];
        }

        bool isBlock(const Grid &p) const
        {
            return !isValidGrid(p) || 0 != buf[p.y * col + p.x];
        }

    public:

        bool save(const std::string &mapPath);

        bool load(const std::string &mapPath);

    public:
        friend void to_json(nlohmann::json &j, const Map &state);
        friend void from_json(const nlohmann::json &j, Map &state);

        // Encode Map to json-style string
        std::string encode() const;

        // Decode Map from json-style string
        Map &decode(const std::string& data_msg);

    private:
        /* Run-Length Encoding */
        std::string compress() const;
        Map& decompress(const std::string& str);

    public:
        Map::State getMapState() const;

        // Get Sub Map
        // Input:   Left Top Coord (Included)
        //          Right Bottom Coord(Not included)
        //          Sub Map ID
        // . 0 1 2 3 4
        // 0 HHHH
        // 1 HHHH       --> getSubMap({0,0}, {2,3})
        // 2 HHHH
        // 3
        Map getSubMap(const Grid& LeftTop, const Grid& RightBottom, uint32_t _id = 0) const;

    public:
        bool show();

        // Character Drawing
        // max output size: 80 x 80
        friend std::ostream &operator<<(std::ostream &os, const Map &map);
    };
}